'''
游戏设计：手绘圆的标准性判断    
版本：vol.12
作者：20242215 贾瑞宁
'''

'''
历史改进：
随机色彩（可以多次画圆而不会混淆，减少清空次数）
检查拟合圆的周长是否超过绘制轨迹长度的两倍
使用圆的参数方程来计算圆的周长，而不是使用所有点的距离之和
使用微软雅黑字体
加入历史记录“排行榜”功能
加入数据库，能够在关闭后程序后储存历史记录
'''

'''
需要下载的包：

1.
pip install numpy
或清华镜像pip install -i https://pypi.tuna.tsinghua.edu.cn/simple numpy 

2.
pip install scipy
或清华镜像pip install -i https://pypi.tuna.tsinghua.edu.cn/simple scipy

PS：
如果尝试某一个下载途径太慢，尝试换途径前，中断当前下载的方法：Ctrl + C

'''

import tkinter as tk
import numpy as np
import math
from scipy.optimize import least_squares
import random
from tkinter import ttk
import sqlite3
import datetime


class CircleDrawingApp:

    def __init__(self, root):
        self.root = root        # 创建画布  root: Tkinter的根窗口对象。

        # 设置窗口标题
        self.root.title("绘制圆估算π")

        # 创建开始界面
        self.start_frame = tk.Frame(root)
        self.start_frame.pack(fill=tk.BOTH, expand=True)  # 创建一个Frame（框架）作为开始界面，并使用pack方法将其放置在根窗口中

        self.start_label = tk.Label(self.start_frame, text="★★★画圆小游戏★★★", font=("Microsoft YaHei", 24))
        self.start_label.pack(pady=20)    # 添加一个标签Label显示游戏标题“★★★画圆小游戏★★★”

        self.rules_label = tk.Label(self.start_frame,
                                    text="\n游戏规则：\n1. 使用鼠标在画布上绘制一个圆。\n2. 绘制完成后，程序将估算π值并显示评级。\n3. 尽量绘制一个完美的圆，以获得更高的评级。\n     PS： 请尽量使圆首尾相接，且不要画的太小^_^     ",
                                    font=("Microsoft YaHei", 16))
        self.rules_label.pack(pady=20)    # 添加另一个标签Label显示游戏规则

        self.start_button = tk.Button(self.start_frame, text="开始游戏", font=("Microsoft YaHei", 16),
                                      command=self.start_game)
        self.start_button.pack(pady=20)  # 添加一个按钮Button，点击后调用start_game方法开始游戏

        # 创建游戏界面
        self.game_frame = tk.Frame(root)  # 创建另一个Frame作为游戏界面，但初始时不显示

        self.canvas = tk.Canvas(self.game_frame, width=600, height=400, bg="white")
        self.canvas.grid(row=0, column=0, columnspan=2)  # 使用grid布局创建一个画布Canvas，用于绘制圆
        self.result_label = tk.Label(self.game_frame, text="画一个圆，松开鼠标查看结果")
        self.result_label.grid(row=1, column=0, columnspan=2) # 使用grid布局创建一个标签Label，用于显示游戏结果
        self.clear_button = tk.Button(self.game_frame, text="清空", command=self.clear_canvas)
        self.clear_button.grid(row=2, column=0, padx=5, pady=5)  # 使用grid布局创建一个按钮Button，点击后调用clear_canvas方法清空画布
        self.leaderboard_button = tk.Button(self.game_frame, text="排行榜", command=self.show_leaderboard)
        self.leaderboard_button.grid(row=2, column=1, padx=5, pady=5)  # 使用grid布局创建另一个按钮Button，点击后调用show_leaderboard方法显示排行榜

        # 初始化变量
        self.drawing = False  # 标记是否正在绘制
        self.points = []  # 存储绘制的点的坐标
        self.line_id = None  # 存储线条的ID
        self.next_color = self.random_color()  # 初始化下一个颜色，通过random_color方法生成这个随机颜色

        # 初始化数据库
        self.init_db()  # 调用init_db方法初始化数据库，用于储存每次绘制的结果，并做成排行榜（可以在关闭后仍保存）

        # 绑定事件
        self.canvas.bind("<ButtonPress-1>", self.start_drawing)  # 绑定鼠标按下事件<ButtonPress-1>，调用self.start_drawing方法开始绘制
        self.canvas.bind("<B1-Motion>", self.draw)  # 绑定鼠标移动事件<B1-Motion>，调用self.draw方法绘制线条
        self.canvas.bind("<ButtonRelease-1>", self.stop_drawing)  # 绑定鼠标释放事件<ButtonRelease-1>，调用self.stop_drawing方法停止绘制

        # 显示开始界面
        self.show_start_screen()

        # 开始炫彩变色效果
        self.change_color()

    def show_start_screen(self):
        self.start_frame.pack(fill=tk.BOTH, expand=True)
        self.game_frame.pack_forget()

    def start_game(self):
        self.start_frame.pack_forget()
        self.game_frame.pack(fill=tk.BOTH, expand=True)

    def start_drawing(self, event):
        self.drawing = True  # 设置绘制状态为True
        self.points = [(event.x, event.y)]  # 初始化点列表
        self.line_id = self.canvas.create_line(event.x, event.y, event.x, event.y, smooth=True,
                                               fill=self.next_color)  # 创建线条

    def draw(self, event):
        if self.drawing:
            # 添加当前点并更新线条
            self.points.append((event.x, event.y))
            self.canvas.coords(self.line_id, *self.get_smoothed_points())  # 更新线条坐标

    # 画完圆，进行分析，并生成下一个随机颜色
    def stop_drawing(self, event):
        if self.drawing:
            self.drawing = False  # 设置绘制状态为False
            self.analyze_circle()  # 调用分析圆的方法
            self.next_color = self.random_color()  # 生成下一个颜色


    #分析圆
    def analyze_circle(self):

        # 检测是否画的太小
        if len(self.points) < 3:
            self.result_label.config(text="至少需要3个点来拟合圆")
            return

        # 拟合圆
        try:
            # 初始猜测
            initial_guess = [0, 0, 1]  # [x_center, y_center, radius]

            # 定义残差函数
            def residual_function(params):
                x_center, y_center, radius = params
                residuals = []
                for x, y in self.points:
                    residuals.append((x - x_center) ** 2 + (y - y_center) ** 2 - radius ** 2)
                return residuals

            # 使用非线性最小二乘法拟合圆
            result = least_squares(residual_function, initial_guess)
            x_center, y_center, radius = result.x

        except Exception as e:
            self.result_label.config(text=f"错误: {str(e)}")
            return

        # 计算实际周长（包含首尾连接）
        circumference = 0.0
        for i in range(len(self.points)):
            x1, y1 = self.points[i]
            x2, y2 = self.points[(i + 1) % len(self.points)]  # 自动闭合路径
            circumference += np.hypot(x2 - x1, y2 - y1)

        # 检查拟合圆的周长是否超过绘制轨迹长度的1.5倍（超过则判定画的不是圆）
        if circumference * 1.5 < 2 * math.pi * radius:
            self.result_label.config(text="请确认您绘制的是否是圆")
            return

        # 计算参数
        diameter = 2 * radius   # 直径
        if diameter == 0:
            self.result_label.config(text="错误: 直径为零")
            return

        calculated_pi = abs(circumference / diameter)   # 计算π的近似值
        error_percent = abs(calculated_pi - math.pi) / math.pi * 100  # 计算误差百分比

        # 评级
        grade = self.calculate_grade(error_percent)

        # 检查误差是否过大（圆是否过于离谱）
        if grade == "F":
            self.result_label.config(text="请确认您绘制的是否是圆")
            return

        # 保存成绩和时间
        current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.save_score(current_time, calculated_pi, error_percent, grade)

        # 显示结果
        self.result_label.config(
            text=f"估算π值: {calculated_pi:.4f}\n"
                 f"误差: {error_percent:.2f}%\n"
                 f"评级: {grade}"
        )

        # 绘制拟合圆
        self.canvas.create_oval(
            x_center - radius, y_center - radius,
            x_center + radius, y_center + radius,
            outline=self.next_color, width=4
        )

    def calculate_grade(self, error_percent):
        # 如果错误率小于3.0，返回等级"A+"
        if error_percent < 3.0:
            return "A+"
        # 如果错误率小于5.0，返回等级"A-"
        elif error_percent < 5.0:
            return "A-"
        # 如果错误率小于8.0，返回等级"B+"
        elif error_percent < 8.0:
            return "B+"
        # 如果错误率小于10.0，返回等级"B-"
        elif error_percent < 10.0:
            return "B-"
        # 如果错误率小于15.0，返回等级"C"
        elif error_percent < 15.0:
            return "C"
        # 如果错误率小于20.0，返回等级"D"
        elif error_percent < 20.0:
            return "D"
        # 如果错误率小于60.0，返回等级"E"
        elif error_percent < 60.0:
            return "E"
        # 如果错误率大于或等于60.0，返回等级"F"
        else:
            return "F"

    # 使用贝塞尔曲线平滑算法，用于平滑绘制的曲线，使其更好看（否则圆的边缘锯齿化很明显）
    def get_smoothed_points(self):
        if len(self.points) < 4:
            return [p for point in self.points for p in point]    # 首先检查点的数量是否少于4个。如果少于4个点，直接返回这些点，因为无法形成贝塞尔曲线

        smoothed_points = []  # 创建一个空列表 smoothed_points 来存储平滑后的点。

        for i in range(1, len(self.points) - 2):
            p0, p1, p2, p3 = self.points[i - 1], self.points[i], self.points[i + 1], self.points[i + 2]
            t_values = np.linspace(0, 1, 20)
            for t in t_values:
                x = (1 - t) ** 3 * p0[0] + 3 * (1 - t) ** 2 * t * p1[0] + 3 * (1 - t) * t ** 2 * p2[0] + t ** 3 * p3[0]
                y = (1 - t) ** 3 * p0[1] + 3 * (1 - t) ** 2 * t * p1[1] + 3 * (1 - t) * t ** 2 * p2[1] + t ** 3 * p3[1]
                smoothed_points.extend([x, y])

        return smoothed_points

    def clear_canvas(self):
        self.canvas.delete("all")  # 清空画布
        self.result_label.config(text="画一个圆，松开鼠标查看结果")  # 重置结果标签
        self.points = []  # 清空点列表
        self.drawing = False  # 重置绘制状态
        self.line_id = None  # 重置线条ID

    # 生成随机颜色
    def random_color(self):
        return "#{:06x}".format(random.randint(0, 0xFFFFFF))


    # 炫彩变色
    def change_color(self):
        self.start_label.config(fg=self.random_color())
        self.root.after(500, self.change_color)  # 每隔500毫秒更新标题标签的颜色

    # 排行榜
    def show_leaderboard(self):
        leaderboard_window = tk.Toplevel(self.root)
        leaderboard_window.title("排行榜")

        # 创建一个表格来显示成绩
        columns = ("排名", "时间", "估算π值", "误差", "评级")
        leaderboard_tree = ttk.Treeview(leaderboard_window, columns=columns, show="headings")
        leaderboard_tree.pack()

        # 设置列的标题
        for col in columns:
            leaderboard_tree.heading(col, text=col)

        # 设置列的宽度和对齐方式
        leaderboard_tree.column("排名", width=50, anchor="center")
        leaderboard_tree.column("时间", width=150, anchor="center")
        leaderboard_tree.column("估算π值", width=100, anchor="center")
        leaderboard_tree.column("误差", width=100, anchor="center")
        leaderboard_tree.column("评级", width=50, anchor="center")

        # 从数据库读取成绩
        self.cursor.execute('SELECT time, pi_estimate, error_percent, grade FROM scores ORDER BY error_percent ASC')
        scores = self.cursor.fetchall()

        # 将成绩插入到排行榜表格中
        for index, (time, pi, error, grade) in enumerate(scores):
            leaderboard_tree.insert("", "end", values=(index + 1, time, f"{pi:.4f}", f"{error:.2f}", grade))

    def init_db(self):
        # 连接到SQLite数据库，如果数据库不存在，会自动创建一个新的数据库文件
        self.conn = sqlite3.connect('scores.db')
        self.cursor = self.conn.cursor()

        # 创建成绩表
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS scores (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                time TEXT NOT NULL,
                pi_estimate REAL NOT NULL,
                error_percent REAL NOT NULL,
                grade TEXT NOT NULL
            )
        ''')
        self.conn.commit()  # 提交更改

    def save_score(self, time, pi_estimate, error_percent, grade):
        self.cursor.execute('''
            INSERT INTO scores (time, pi_estimate, error_percent, grade) VALUES (?, ?, ?, ?)
        ''', (time, pi_estimate, error_percent, grade))
        self.conn.commit()

    # 关闭数据库连接
    def __del__(self):
        self.conn.close()

root = tk.Tk()
app = CircleDrawingApp(root)
root.mainloop()